package br.com.comanda.dao.autosql;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.util.Log;
import br.com.comanda.dao.autosql.annotations.Column;
import br.com.comanda.dao.autosql.annotations.Id;
import br.com.comanda.dao.autosql.annotations.Unique;
import br.com.comanda.dao.autosql.exception.AutoSQLException;
import br.com.comanda.dao.autosql.exception.ConstraintViolatedException;
import br.com.comanda.dao.autosql.validation.CheckValidation;
import br.com.comanda.dao.script.RepositoryScript;
import br.com.comanda.util.CollectionUtils;
import br.com.comanda.util.Constantes;
import br.com.comanda.util.DateUtils;
import br.com.comanda.util.reflection.MethodAccess;
import br.com.comanda.util.reflection.ReflectionUtils;

/**
 * Repositorio generico que prove funcionalidades para o select e insert no
 * SQLite.
 * 
 * @author thiago
 * @since 09/01/2013
 */
public final class GenericRepository extends RepositoryScript {

	/**
	 * Pega SQLiteDatabase para realizar o select e save
	 */
	private static Context context;
	private Class<?> parametrizedClass;

	private GenericRepository(Context context) {
		super(context);
	}

	/**
	 * Garante que havera apenas um objeto repositorio usado pela thread
	 * principal do android
	 */
	public static GenericRepository getInstanceFor(Class<?> clazz) {
		if (context == null) {
			throw new IllegalArgumentException(
					"O contexto nao foi setado ao GenericRepository. "
							+ "Deve-se chamar antes a função GenericRepository.setContext()");
		}
		GenericRepository repositorio = new GenericRepository(context);
		repositorio.parametrizedClass = clazz;
		return repositorio;
	}

	public Cursor rawQuery(String query) throws AutoSQLException {
		try {
			return database().rawQuery(query, null);
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	/**
	 * Realiza uma busca de uma lista mas com cada objeto simplificado.
	 */
	public List<?> findSimpleList(String where) throws AutoSQLException {
		try {
			Cursor cursor = database().query(
					SQLBuilder.tableName(parametrizedClass),
					SQLBuilder.getSelectFields(parametrizedClass, true), where,
					null, null, null, null);
			return (List<?>) RowMapper.fetchCursor(parametrizedClass, cursor,
					true);
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	/**
	 * Realiza uma busca de um objeto simplificado. Apenas os campos essenciais
	 * são buscados.
	 */
	public Object findSimpleObject(String where) throws AutoSQLException {
		try {
			Cursor cursor = database().query(
					SQLBuilder.tableName(parametrizedClass),
					SQLBuilder.getSelectFields(parametrizedClass, true), where,
					null, null, null, null);

			List<?> list = (List<?>) RowMapper.fetchCursor(parametrizedClass,
					cursor, true);
			return CollectionUtils.getFirst(list);
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	/**
	 * Realiza uma busca retornando uma lista de objetos.
	 */
	public List<?> findList(String where) throws AutoSQLException {
		try {
			Cursor cursor = database().query(
					SQLBuilder.tableName(parametrizedClass),
					SQLBuilder.getSelectFields(parametrizedClass, false),
					where, null, null, null, null);

			return (List<?>) RowMapper.fetchCursor(parametrizedClass, cursor,
					false);
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	/**
	 * Realiza uma busca retornando apenas um objeto
	 */
	public Object findObject(String where) throws AutoSQLException {
		try {
			Cursor cursor = database().query(false,
					SQLBuilder.tableName(parametrizedClass),
					SQLBuilder.getSelectFields(parametrizedClass, false),
					where, null, null, null, null, "1");

			List<?> list = (List<?>) RowMapper.fetchCursor(parametrizedClass,
					cursor, false);
			return CollectionUtils.getFirst(list);
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	public Object findById(long id) throws AutoSQLException {
		try {
			Field f = getFieldId();
			return findObject(String.format("%s = " + id,
					f.getAnnotation(Column.class).name()));
		} catch (Exception e) {
			throw new AutoSQLException(e.getMessage(), e);
		}
	}

	private Field getFieldId() throws AutoSQLException {
		for (Field f : parametrizedClass.getDeclaredFields()) {
			if (f.isAnnotationPresent(Id.class)) {
				return f;
			}
		}

		throw new AutoSQLException("Nenhum campo esta anotado com @Id");
	}

	/**
	 * Persiste um objeto no banco
	 */
	private void persist(Object object) throws AutoSQLException {
		try {
			final Class<?> clazz = object.getClass();
			final String table = SQLBuilder.tableName(clazz);
			final ContentValues values = createContentValues(object);

			long id = database().insertOrThrow(table, "", values);

			try {
				Field f = getFieldId();

				if (Modifier.isPublic(f.getModifiers())) {
					f.set(object, id);
				} else {
					Method method = ReflectionUtils.getMethod(
							object.getClass(), MethodAccess.SET, f);
					method.invoke(object, new Object[] { id });
				}
			} catch (Exception e) {
				throw new AutoSQLException("Houve um erro ao persistir objeto",
						e);
			}
		} catch (SQLiteConstraintException e) {
			throw e;
		} catch (Exception e) {
			throw new AutoSQLException("Houve um erro ao persistir objeto", e);
		}
	}

	/**
	 * Atualiza um registro no banco de dados
	 */
	private void update(Object object, String where, String chave)
			throws AutoSQLException {
		/*
		 * if(object instanceof Synchronizable) { ((Synchronizable)
		 * object).setSincronizado(false); }
		 */

		final Class<?> clazz = object.getClass();
		final ContentValues contentValue = createContentValues(object);
		final String tableName = SQLBuilder.tableName(clazz);

		database().update(tableName, contentValue,
				String.format("%s = ?", where), new String[] { chave });
	}

	public void persistOrMerge(Object object) throws AutoSQLException {
		try {
			CheckValidation.isValid(object, parametrizedClass);
			Field f = getFieldId();

			String id = null;

			if (Modifier.isPublic(f.getModifiers())) {
				id = String.valueOf(f.get(f));
			} else {
				Method m = ReflectionUtils.getMethod(parametrizedClass,
						MethodAccess.GET, f);
				id = String.valueOf(m.invoke(object));
			}

			if (id.equals("0")) {
				persist(object);
			} else {
				update(object, ((Column) f.getAnnotation(Column.class)).name(),
						id);
			}
		} catch (SQLiteConstraintException e) {
			throw new ConstraintViolatedException(
					discoverConstraintExceptionReason(object), e);
		} catch (Exception e) {
			throw new AutoSQLException("Houve um erro no persisteOrMerge", e);
		}
	}

	private String discoverConstraintExceptionReason(Object object) {
		Field[] fields = object.getClass().getDeclaredFields();

		for (Field f : fields) {
			if (f.isAnnotationPresent(Unique.class)) {
				return String.format(
						"O campo %s já possui um registro no banco com esse mesmo valor. "
								+ "Verifique pois esse valor deve ser único.",
						f.getAnnotation(Unique.class).nome());
			}
		}

		return "Você está cadastrando um campo que já existe salvo no banco de dados. Verifique";
	}

	/**
	 * Remove registros da base de dados de acordo com clausula where e seus
	 * argumentos.
	 * 
	 * Retorna a quantidade de registros que foram removidos
	 */
	public int remove(String where, String... whereArgs)
			throws AutoSQLException {
		return database().delete(SQLBuilder.tableName(parametrizedClass),
				where, whereArgs);
	}

	/**
	 * Executa uma SQL nativa
	 */
	public void executeSQL(String sql) throws AutoSQLException {
		try {
			database().execSQL(sql);
		} catch (Exception e) {
			throw new AutoSQLException(
					"Houve um erro ao executar comando SQL: " + sql, e);
		}
	}

	public void beginTransaction() throws AutoSQLException {
		try {
			database().beginTransaction();
		} catch (Exception e) {
			throw new AutoSQLException(
					"Houve um problema com o banco de dados. Reinicie o sistema e tente novamente.",
					e);
		}
	}

	public void endTransaction() throws AutoSQLException {
		try {
			database().endTransaction();
		} catch (Exception e) {
			throw new AutoSQLException(
					"Houve um problema com o banco de dados. Reinicie o sistema e tente novamente.",
					e);
		}
	}

	public void setTransactionSuccessful() throws AutoSQLException {
		try {
			database().setTransactionSuccessful();
		} catch (Exception e) {
			throw new AutoSQLException(
					"Houve um problema com o banco de dados. Reinicie o sistema e tente novamente.",
					e);
		}
	}

	/**
	 * Cria o objeto ContentValue para adicionar no banco de dados
	 */
	private ContentValues createContentValues(Object clazz)
			throws AutoSQLException {
		try {
			ContentValues contentValues = new ContentValues();

			Field[] fields = clazz.getClass().getDeclaredFields();

			for (Field f : fields) {
				if (!f.isAnnotationPresent(Column.class)) {
					continue;
				} else {
					Column column = f.getAnnotation(Column.class);
					if (column.insertable()) {
						Method method;
						try {
							if (Modifier.isPublic(f.getModifiers())) {
								addInContentValue(contentValues, column.name(),
										f.get(f));
							} else {
								method = ReflectionUtils.getMethod(
										clazz.getClass(), MethodAccess.GET, f);
								addInContentValue(contentValues, column.name(),
										method.invoke(clazz));
							}
						} catch (Exception e) {
							Log.e(Constantes.TAG_LOG, e.getMessage(), e);
							throw new AutoSQLException(
									"Problema ao realizar comando SQL", e);
						}
					}
				}
			}
			return contentValues;
		} catch (Exception e) {
			throw new AutoSQLException("Problema ao criar contentValues", e);
		}
	}

	/**
	 * Cria o mapa ContentValue para adicionar parametros ao insert
	 */
	private void addInContentValue(ContentValues values, String key,
			Object value) {
		if (value instanceof String) {
			values.put(key, String.valueOf(value));
		} else if (value instanceof Long) {
			values.put(key, (Long) value);
		} else if (value instanceof Integer) {
			values.put(key, (Integer) value);
		} else if (value instanceof Boolean) {
			values.put(key, (Integer) (((Boolean) value) ? 1 : 0));
		} else if (value instanceof Double) {
			values.put(key, (Double) value);
		} else if (value instanceof Short) {
			values.put(key, (Short) value);
		} else if (value instanceof Date) {
			values.put(key, DateUtils.convert((Date) value));
		} else if (value instanceof Character) {
			values.put(key, String.valueOf(value));
		} else if (value instanceof Float) {
			values.put(key, (Float) value);
		}
	}

	public static void setContext(Context context) {
		GenericRepository.context = context;
	}

	public boolean inTransaction() {
		return database().inTransaction();
	}
}
